<?php
/**
 * Auth
 * @filesource
 */
/**
 * Namespace
 */
namespace framework\library\authentication;
/**
 * Dependencies
 */
use framework\library\Model;
use framework\Front;
use framework\helper\UnCaptcha;
/**
 * Auth Class
 * 
 * Features: 
 * * Authenticates across multiple applications. Single sign-on.
 * * 3 Authentication methods
 * * Validates a nonce and regenerates on every attempt
 * * Uses an encrypted server-certificate to validate session data 
 * with returning user
 * * Removes Session Fixation vulnerabilities. Regenerates session IDs on 
 * significant changes to user authentication profile as well as after 
 * configured number of site visits.
 * * Locks user account after configured missed attempt count
 * 
 * 
 * The class can authenticate with any one of three methods.  A traditional
 * plain text method classically used with SSL connections, a method similar
 * to captcha that is referred to here as unCaptcha since it takes remarkably
 * different approach, and the third is through the use of a hashed password
 * that is hashed on the client.
 *
 * Authentication and Authorization for Plurious
 *
 * @author Randy Casburn <randy@plurious.com>
 * @copyright Copyright Plurious Software, all rights reserved
 * @license MIT
 * @license http://www.opensource.org/licenses/mit-license.html
 * @link http://plurious.com/framework/documentation/classes/Front.html
 * @version 2.1
 * 
 * @uses framework\library\Model
 * @uses framework\Front
 * @uses framework\helper\UnCaptcha
 */
class Auth extends Model {

    /** Name of the database table used for authentication */
    private $auth_table;

    /** The Session Object reference */
    protected $session;

    /** The Document Object reference */
    protected $document;

    /** The Config Object reference */
    protected $config;

    /** Name of the User ID column name from the configuration */
    protected $useridCol;

    /** Name of the username column name from the configuration */
    protected $usernameCol;

    /** Name of the password column name from the configuration */
    protected $passwordCol;

    /** Name of the nicename column name from the configuration */
    protected $nicenameCol;

    /**
     * Builds all the necessary preliminary components required for the 
     * athentiation process.
     */
    public function __construct() {
        parent::__construct();
        $this->registry = Front::getInstance();
        $this->session = $this->registry->get('session');
        $this->document = $this->registry->get('document');
        $this->request = $this->registry->get('request');
        $this->config = $this->registry->get('config');
        $this->config->load(dirname(__FILE__) . '/auth_config');
        $this->auth_table = $this->config->get('users_table');
        $this->useridCol = $this->config->get('UserIdCol');
        $this->usernameCol = $this->config->get('UserNameCol');
        $this->passwordCol = $this->config->get('PasswordCol');
        $this->nicenameCol = $this->config->get('NiceNameCol');
        $this->session->set('seed', $this->generateSeed());
        $enhance = $this->config->get('auth_password_enhance');
        if ($enhance) {
            $pwi = new unCaptcha();
            $pw_enhance = array('enhance' => true, 'placement' => $pwi->placement, 'char' => $pwi->char);
            $pw_enhance_output = $pwi->rules.$pwi->img;
            $this->session->set('pw_enhance_output', $pw_enhance_output);
            if($this->session->get('pw_enhance')){
                $this->session->set('pw_enhance_validation', $this->session->get('pw_enhance'));
            } 
            $this->session->set('pw_enhance', $pw_enhance);
            $this->config->set('auth_offer_remember', false);
        } else {
            if (!$this->session) {
                $this->registry->loadClass('framework\library\session');
            }
            $session = $this->registry->get('session');
            $session->set('pw_enhance_validation', '');
        }
    }
    
    /**
     * Generate a new seed (nonce) to be used for authentication.
     * 
     * @returns a new seed
     */
    private function generateSeed(){
        return hash('sha256', date(time()));
    }

    /**
     * Checks the database (final authority) to see if a user is locked
     * 
     * @param string $username Any username
     * @return boolean True=Locked
     */
    public function isUserLocked($username) {
        $table = $this->config->get('auth_table');
        $where = array($usernameCol => $this->usernameCol);
        $result = $this->selectCols($table, array($usernameCol, 'is_active', 'is_locked'), $where);
        return $result[0]['is_locked'];
    }

    /**
     * Checks the database (final authority) to find all users with the given
     * nice name
     * 
     * If no parameter provided, searches for the current user
     * 
     * @param string $nicename The nice name to search 
     * @return array Associative array of all records matching search
     */
    public function findUserMatchesNiceName($nicename = '') {
        $nicename = ($nicename) ? $nicename : $this->session->get('username');
        $table = $this->config->get('auth_table');
        $where = array($this->nicenameCol => $nicename, 'is_active' => 1, 'is_locked' => 0);
        return $this->selectCols($table, array($this->usernameCol, $this->nicenameCol, 'is_active', 'is_locked'), $where);
    }

    /**
     * Checks the database (final authority) to find all users with the given
     * username (should only be one)
     * 
     * If no parameter provided, searches for the current user
     * 
     * @param string $username The username to search 
     * @return array Associative array of all records matching search
     */
    public function findUserMatches($username = '') {
        $username = ($username) ? $username : $this->session->get('username');
        $columns = array($this->usernameCol, $this->nicenameCol, $this->passwordCol, $this->useridCol, 'contexts', 'roles', 'is_active', 'is_locked');
        $table = $this->config->get('users_table');
        $where = array($this->usernameCol => $username, 'is_active' => 1, 'is_locked' => 0);
        return $this->selectCols($table, $columns, $where);
    }

    /**
     * Lock the user account.
     * 
     * @param string $username
     * @return boolean True=record updated successfully
     */
    public function lockUser($username) {
        $table = $this->config->get('auth_table');
        $data = array('is_locked' => 1);
        $where = array($this->usernameCol => $username);
        return $this->update($data, $table, $where);
    }

    /**
     * Sets a new date for the latest login in time
     * 
     * @param string $username
     * @return boolean True=record updated successfully
     */
    public function updateUserLastLogin($username) {
        $table = $this->config->get('auth_table');
        $data = array('lastlogin' => date('YmdHis', time()));
        $where = array($this->usernameCol => $username);
        return $this->update($data, $table, $where);
    }

    /**
     * Sets a new date for the latest activity
     * 
     * @todo Needs to be updated to reflect new API
     * @return boolean True=record updated successfully
     */
    public function last_activity() {
        $this->where('last_activity >', $this->CI->session->now - $this->CI->session->sess_expiration);
        return $this->get($this->sess_table_name);
    }

    /**
     * Returns all records with unique IDs
     * 
     * @return array Associative array of all records with unique IDs
     */
    public function match_unique_ids() {
        return $this->select($this->auth_table, array($this->usernameCol, $this->useridCol), array('id', 'UNIQUE'));
    }

    /**
     * Authenticate a user
     * 
     * @param string $username The username to authenticate
     * @param string $hash the password to authenticate (may or may not be a hash
     *
     */
    public function authenticate($username, $hash) {
        if (!$this->session->get('seed')) {
            echo "The seeds are out of sync. Refresh the page.";
            exit;
        }
        $attempts = $this->session->get('login_attempts');
        $this->session->set('login_attempts', ($attempts) ? $attempts + 1 : 1);
        $this->session->set('sid', session_id());
        // retrieve the user record from the database
        $user = $this->findUserMatches($username);
        // retrieve the seed from the database based upon the ID stored in the session
        $seed = $this->session->get('seed');

        if (!$user || !$seed) {
            return $this->updateAttempts(AUTH_FAILURE_IDENTITY_NOT_FOUND);
        }
        if (count($user) > 1) {
            return $this->updateAttempts(AUTH_FAILURE_IDENTITY_AMBIGUOUS);
        }

        $this->user = $user = $user[0];

        if ($user['is_locked'] == 1) {
            return $this->updateAttempts(AUTH_FAILURE_ACCOUNT_LOCKED);
        }
        if ($user['is_active'] == 0) {
            return $this->updateAttempts(AUTH_FAILURE_ACCOUNT_INACTIVE);
        }
        /**
         * Now we check if the account is authorized in the current context
         */
        $user['contexts'] = unserialize(stripslashes($user['contexts']));
        if (!in_array($this->config->get('context'), $user['contexts']))
            return $this->updateAttempts(AUTH_FAILURE_IDENTITY_NOT_FOUND);
        /**
         * Since we have all the data we need, let's performance optimize here
         * and simply add the user's role heirarchy to the user object
         * 
         * roles -> an array thus: 
         *        context            roles          context         roles
         * array( 'web'=>array('member', 'editor'), 'admin'=>('root','context','acct'))
         * 
         * see the role class for the isA() tests (the __call() magic method
         */
        $user['roles'] = unserialize(stripslashes($user['roles']));

        /**
         * A seed (nonce) is always sent to the form and retrieved from the form.
         * The user MUST set a hidden field with the value of a seed.
         */
        if (!$this->request->getPost($this->config->get('seedLabel'))) {
            echo "This hacking attempt from your IP address has been logged";
            exit;
        }
        // Finally get to test the password
        if(!$this->validatePassword($hash)){
            return $this->updateAttempts(AUTH_FAILURE_CREDENTIAL_INVALID);
        } 
        // If we made it this far all is good..log them in
        $user['remember'] = $this->session->get('remember');
        $this->storeWrite($user);

        return AUTH_SUCCESS;
    }
    
    private function validatePassword($password){
        $salt = substr($this->user['pw'], 0, 64);
        $valid_hash = substr($this->user['pw'], 64, 64);
        /**
         * NOTE:  There are three ways to authenticate with this library:
         * 
         * 1. The traditional method sends both username and password in the clear and
         * hashes them locally so it can be checked against the DB stored hash
         * 
         * 2. Using a password enhancement technique that is similar to a captcha
         * but it isn't -- look for the unCaptcha.php file for information
         *
         */
        // pwi = password instructions //
        $pwi = $this->session->get('pw_enhance_validation');
        // Authentication method 2: Enhanced (unCaptcha) login process
        if (!empty($pwi) && $pwi['enhance']) {
            // Remove the enhancement character from the password
            // Get the part of the password prior to the enhancement character
            $test_pw = substr($password, 0, $pwi['placement']-1);
            // If the enhancement is not the last character get everything
            // after the enhancement character
            if($pwi['placement'] !== strlen($password)){
                $test_pw .= substr($password, $pwi['placement']);
            }
            $test_hash = $this->hashPassword($test_pw, $salt);
        } else {
            // Authentication method 1: Traditional clear text method - hash inbound clear-text password/compare against hashed db password
            $test_hash = $this->hashPassword($password, $salt);
        }
        if($test_hash === $valid_hash){
            return true;
        } else {
            return false;
        }
    }
    
    private function hashPassword($password, $salt){
        return hash('sha256', $salt.$password);
        
    }
    
    private function changePassword($password){
        $salt = $this->session->get('seed');
        $hash = $this->hashPassword($password, $salt);
        $combined = $salt.$hash;
        $values = array($this->passwordCol => $combined);
        $where = array($this->usernameCol =>$this->user['username']);
        $this->update($values, $this->auth_table, $where);
    }

    /**
     * We test for repeated bad attempts and strike them down if they exceed the
     * limit set in the config.  We'll use session flash to keep track of this.
     * The incrementing of the counter actually happens in the _store_write method where
     * the session information is updated.
     *
     * @param int $result a success or failure mode to test against
     * @return int the success or failure mode to tested against
     */
    private function updateAttempts($result) {
        /**
         * All results less than 1 are failure modes
         */
        if ($result < 1) {
            // If this count is exceeded...lock the user's account
            $autoLockCount = $this->config->get('auth_auto_lock_after');
            // If autoLockCount is zero (or equal to False) then no locking will ever occur
            if ($autoLockCount) {
                // Current number of attemps
                $attempts = $this->session->get('login_attempts');
                // This means: If $autoLockCount  is set to 3 the user will not be able to log in after the THIRD 
                // attempt.  In other words, they get THREE genuine trys. 
                if ($attempts >= $autoLockCount) {
                    $locked = $this->lockUser();
                }
            }
        }
        return $result;
    }

    /**
     * Logs the user out and redirects them as appropriate
     * 
     * @param string $redirectTo a URL to redirect to upon logout
     */
    public function logout($redirectTo = ROOT_URL) {
        $this->session->removeUser();
        $this->document->redirect($redirectTo);
    }

    /**
     * Retrieves the user id for a given username
     * 
     * @param string $username The username to get the id of
     * @return mixed Returns boolean false if user not found or user ID otherwise
     */
    public function getUserId($username) {
        $result = $this->db->selectCols($user_table, array($this->config->get('UserNameCol') => $username));
        if ($result->num_rows() == 1) {
            $rows = $result->fetch_array();
            return $rows[0][$this->useridCol];
        }
        return FALSE;
    }

    /**
     * Validates that the data in the session right now is the same as
     * the data that was in the session on the previous visit. It also validates
     * that the current user credentials belong to the same session currently
     * being used.
     * 
     * @param array $data Holds the previous CRC check and session data needed to recalulate the verification
     * @return boolean to indicate if the test passed
     */
    private function serverCert(array $data) {
        $valid = @crc32(@$data['id'] . (string) $this->config->get('server_certificate') . @$data['p']) == @$data['t'];
        return $valid;
    }

    /**
     * Fetches the 'remember me' cookie and the session user data so they can
     * be validated
     * 
     * This is part of the 'remember me' functionality
     * 
     * @return array The data to validate
     */
    private function getValidationData() {
        // First try to get the remember me cookie...
        // It is possible that the cookie has not been set. Ordinarily this gets
        // done via a redirect so we can check the cookie.  
        // 
        $validation = $this->session->getUserData('validation');
        if (!empty($validation)) {
            return $validation;
        }
    }

    /**
     * Upon successful login, writes the user data to the session and creates
     * the 'remember me' cookie if requested.  
     * 
     * Also stores all user data as Obj properties.
     * 
     */
    private function storeWrite(Array $user) {
        $id = $user['id'];
        $p = md5(uniqid(rand(), true));
        $t = crc32($id . (string) $this->config->get('server_certificate') . $p);
        $user['validation'] = (array) compact('id', 'p', 't');
        // no need to store the password in the session or user object
        $user['pw'] = null;
        // Set the expiration of the login
        if ($user['remember'] === true) {
            $cfg_time = strtotime('+' . $this->config->get('auth_expire_after'));
            if($cfg_time > time()){
                $expire = $cfg_time;
            } else {
                $expire = time();
            }
        } else {
            $expire = time() - 3600;
        }
        // Convenience - make this like a real object rather than a model ;-)
        foreach ($user as $key => $val) {
            $this->{$key} = $val;
        }
        $this->session->setUserData($user);
        $this->session->reset($expire);
    }

    /**
     * Checks the user's authentication status upon a revisit to the site
     * 
     * @return boolean True=is authenticated
     */
    public function isAuthenticated() {
        $data = $this->getValidationData();
        $isIn = (!empty($data) &&
                $this->serverCert($data) &&
                isset($data['id']));
        if (!$isIn) {
            $this->session->set('login_message', '');
            return FALSE;
        }
        $this->storeWrite($this->session->getUserData());
        // return user_id
        return $data['id'];
    }

    /**
     * Forces one of two choices: 1. be authenticated to continue or 2. redirect
     * away from the secure resource area.
     * 
     * @param string $url URL to redirec to if not authenticated
     */
    public function requireAuthentication($url) {
        if (!$this->isAuthenticated()) {
            $this->document->redirect($url);
        }
    }

}

?>
